InputSystem基础框架

Key

flowchart LR

Input(PlayerController::UpdateInput)

Input
-->InputProcesssor::Tick
Input
-->InputHandle::Tick
Input
-->InputCollector::Tick

InputProcessor

对于 InputProcessor,进行按键的原始输入处理,调用 InputKey 将将数据传递到 InputCollector,继续后续的操作;

  1. 通过监听引擎原始的按键数据 APlayerController::IsInputKeyDown(const FKey Key)
  2. 允许外部调用输入,执行 InputKey (比如某个 UI 点击触发输入);

特别地,在 Tick 中进行原始数据处理;

由于一般按键输入支持 Ctrl / Shit / Alt / Cmd + Key,需要对这种情况适配,同时防止按键冲突:对于KeySetting(KeyType - 具体按键配置 映射) 额外维护 bCtrl (需要与 ctrl 组合)MustNoCtrl (需要不按下 Ctrl)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// InputProcessor

void UInputProcessor::ProcessInputKey() // Tick
{
bool bShift = PC->IsInputKeyDown(EKeys::LeftShift) || PC->IsInputKeyDown(EKeys::RightShift);
bool bCtrl = PC->IsInputKeyDown(EKeys::LeftControl) || PC->IsInputKeyDown(EKeys::RightControl);
bool bAlt = PC->IsInputKeyDown(EKeys::LeftAlt) || PC->IsInputKeyDown(EKeys::RightAlt);
bool bCmd = PC->IsInputKeyDown(EKeys::LeftCommand) || PC->IsInputKeyDown(EKeys::RightCommand);

auto IsKeyDown = [&](EInputKeyType KeyType) -> bool
{
for (auto KeySetting : KeySettings[KeyType]) // 一个 KeyType 可能有多种触发方式
{
bool bKeyDown = PC->IsInputKeyDown(FKey(FName(KeySetting.KeyName())));

if ((KeySetting.Shift() && !bShift) || (KeySetting.MustNoShift() && bShift))
bKeyDown = false;
else if ((KeySetting.Ctrl() && !bCtrl) || (KeySetting.MustNoCtrl() && bCtrl))
bKeyDown = false;
else if ((KeySetting.Alt() && !bAlt) || (KeySetting.MustNoAlt() && bAlt))
bKeyDown = false;
else if ((KeySetting.Cmd() && !bCmd) || (KeySetting.MustNoCmd() && bCmd))
bKeyDown = false;

if (bKeyDown)
return true;
}

return false;
};

for (auto KeyType : KeyTypes)
{
if (IsKeyDown(KeyType))
{
InputKey(KeyType);
}
}
}

// ----------

void UInputProcessor::InputKey(EInputKeyType KeyType)
{
if (!IsValid(InputCollector)) return;
InputCollector->SetKey(KeyType, true);
}

InputCollector

classDiagram
	direction LR
	
	InputCollector --> FInputKeyInfo
	class InputCollector {
		KeyInfoMap: TMap~KeyType, FInputKeyInfo~
	}
	
	FInputKeyInfo *-- EKeyState
	class FInputKeyInfo {
		bEnable : bool
		bPressed : bool
		KeyState : EKeyState
	}
	
	class EKeyState {
		IKS_Idle : 未按下
		IKS_Press : 当帧按下
		IKS_Release : 当帧释放
		IKS_Holding : 持续按下
	}

对于 InputCollector,维护 Key - Press 相关的原始输入数据,将数据从 Press 加工到 KeyState,并每帧检测是否未按下,这样外部只需要进行 Press,而不需要 Release;有几个主要的功能:

  1. 提供 SetKey 进行按键设置,提供 GetKey 查询按键状态;
  2. 维护每个 KeyKeyInfo,数据用于后续处理;
  3. 提供 EnableKey,设置按键是否可以输入(Disable 时需要取消输入);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// InputKeyInfo

USTRUCT(BlueprintType)
struct FInputKeyInfo
{
GENERATED_BODY()

UPROPERTY()
bool bPressed = false;

UPROPERTY()
EInputKeyState KeyState = IKS_Idle;

UPROPERTY()
bool bEnable = true;

// 用于记录该帧内是否已经处理过按键
bool bProcessedThisFrame = false;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// InputCollector

void UInputCollector::SetKey(EInputKeyType KeyType, bool bPressed, bool bForce)
{
if (bForce || IsEnableKey(KeyType))
{
SetKey_Internal(KeyType, bPressed);
}
}

FInputKeyInfo UInputCollector::GetKeyInfo(EInputKeyType KeyType)
{
return KeyInfoMap.Contains(KeyType) ? KeyInfoMap[KeyType] : {};
}

// ----- Internal -----
void UInputCollector::Tick()
{
// 每次 Tick 进行按键信息重置
for (auto KeyType : KeyTypes)
{
FInputKeyInfo& KeyInfo = KeyInfoMap[KeyType];
if (!KeyInfo.bProcessedThisFrame)
{
SetKey_Internal(KeyType, false);
KeyInfo.bProcessedThisFrame = false;
}
}
}

void UInputCollector::SetKey_Internal(EInputKeyType KeyType, bool bPressed)
{
FInputKeyInfo& KeyInfo = KeyInfoMap[KeyType];

if (bPressed == KeyInfo.bPressed)
{
KeyInfo.KeyState = bPressed ? IKS_Holding : IKS_Idle;
}
else
{
KeyInfo.KeyState = bPressed ? IKS_Press : IKS_Release;
}

KeyInfo.bPressed = bPressed;
KeyInfo.bProcessedThisFrame = true;
}

InputHandle

classDiagram
	class InputHandleBase {
		KeyInfo : TArray~EInputKeyType~
		
		SetInput(KeyType, bPressed)
		CollectInput()
		ProcessInput()
		NotifyInput()
	}
	
	InputHandleBase <|-- InputHandld_Default
	InputHandleBase <|-- InputHandle_Character
	InputHandleBase <|-- InputHandle_Vehicle

业务自定义逻辑的位置,一般仅对业务开放这部分的代码修改;

根据不同的输入模式,创建不同的 InputHandle,在这里对输入进行业务级别的加工,同时处理基本的输入操作回调(内部基本的业务逻辑,以及允许其它模块向内部注册的事件),并将输入操作通知给其它模块;

业务自定义其输入逻辑,比如:

  1. 该输入模式下,当输入某个操作时,系统自动取消其它操作的输入等;
  2. 处理按键是否需要自动抬起;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// InputHandle

void UInputHandleBase::Tick()
{
RecordInput(); // 记录上次输入结果
CollectInput(); // 收集本帧输入结果(先默认清空,后根据具体情况收集)
ProcessInput(); // 根据输入结果修改 KeyState
DispatchInput(); // 分发输入结果并处理
}


// ----- Process -----

void UInputHandleBase::ProcessInput()
{
for (auto Key : ActiveKeyTypes)
{
bool bLastKeyDown = RecordKeyInfoMap[Key].bPressed;
bool bCurrentKeyDown = KeyInfoMap[Key].bPressed;

if (bCurrentKeyDown != bLastKeyDown)
{
bCurrentKeyDown ? OnGameKeyPress(Key) : OnGameKeyRelease(Key);
}
else
{
bCurrentKeyDown ? OnGameKeyHolding(Key) : OnGameKeyIdle(Key);
}
}
}


// ----- Register -----

DECLARE_MULTICAST_DELEGATE_TwoParams(FInputEvent, KeyType, KeyState);
using FInputEventCallbackParamTypes = FInputEventCallbackType::FDelegate::TFuncType;

TMap <KeyType, FInputEvent> ProcessInputEvents;

template <
typename UserClass,
typename FuncType = TMemFunPtrType<TIsConst<UserClass>::Value, UserClass, FInputEventCallbackParamTypes>::Type
>
bool UInputHandleBase::RegisterInputEvent(EInputKeyType KeyType, UserClass* InUserObject, FuncType InFunc)
{
if (!ProcessInputEvents.Contains(KeyType))
ProcessInputEvents.Add(KeyType, {});

auto& Event = Processvents[KeyType];
if (Event.IsBoundToObject(InUserObject))
{
return false;
}

Event.AddUObject(InObject, InFunc);
return true;
}

template <typename UserClass>
bool UnregisterInputEvent(EInputKeyType Type, UserClass* InUserObject)
{
if (!ProcessInputEvents.Contains(Type))
return false;

auto& Event = ProcessInputEvents[Type];
Event.RemoveAll(InUserObject);
return true;
}

void UInputHandleBase::DispatchInput()
{
// 内部处理基础操作
// ...

// 执行外部注册的操作
for (auto KeyType : ActiveKeyTypes)
{
if (auto EventPtr = ProcessInputEvents.Find(KeyType))
{
EventPtr->Broadcast(KeyType, GetKeyState(KeyType));
}
}
}

// ----- Helper -----

void UInputHandleBase::SetInput(EKeyType KeyType)
{
KeyInfo[KeyType].bPressed = true;
}

void UInputHandleBase::UpdateKeyFromCollector(EInputKeyType KeyType) // 在 CollectInput 中调用
{
SetInput(KeyType, InputCollector->GetKeyInfo(KeyType).bPressed);
}

Mouse

InputProcessor

Interface : IInputProcessorFSlateApplication 驱动,也有 KeyMouse 相关操作的回调:

flowchart LR

FSlateApplication::ProcessMouseButtonDownEvent
-->FSlateApplication::InputPreProcessorsHelper::HandleMouseButtonDownEvent
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// IInputProcessor

class SLATE_API IInputProcessor
{
public:
IInputProcessor(){};
virtual ~IInputProcessor(){}

virtual void Tick(const float DeltaTime, FSlateApplication& SlateApp, TSharedRef<ICursor> Cursor) = 0;

// Key...
virtual bool HandleKeyDownEvent(FSlateApplication& SlateApp, const FKeyEvent& InKeyEvent) { return false; }
virtual bool HandleKeyUpEvent(FSlateApplication& SlateApp, const FKeyEvent& InKeyEvent) { return false; }
virtual bool HandleAnalogInputEvent(FSlateApplication& SlateApp, const FAnalogInputEvent& InAnalogInputEvent) { return false; }

// Mouse...
virtual bool HandleMouseMoveEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) { return false; }
virtual bool HandleMouseButtonDownEvent( FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) { return false; }
virtual bool HandleMouseButtonUpEvent( FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) { return false; }
virtual bool HandleMouseButtonDoubleClickEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) { return false; }
virtual bool HandleMouseWheelOrGestureEvent(FSlateApplication& SlateApp, const FPointerEvent& InWheelEvent, const FPointerEvent* InGestureEvent) { return false; }
virtual bool HandleMotionDetectedEvent(FSlateApplication& SlateApp, const FMotionEvent& MotionEvent) { return false; };

/** Debug name for logging purposes */
virtual const TCHAR* GetDebugName() const { return TEXT(""); }
};

可以在合适的地方(比如 InputSubSystem / InputManager )进行创建:

1
2
InputProcessor = MakeShareable(new FInputProcessor());
FSlateApplication::Get().RegisterInputPreProcessor(InputProcessor);

其次是与 UI 的交互, UserWidget 中也提供了 Mouse 的回调:

1
2
3
4
virtual FReply NativeOnTouchStarted(const FGeometry& InGeometry, const FPointerEvent& InGestureEvent) override;
virtual FReply NativeOnTouchMoved(const FGeometry& InGeometry, const FPointerEvent& InGestureEvent) override;
virtual FReply NativeOnTouchEnded(const FGeometry& InGeometry, const FPointerEvent& InGestureEvent) override;
virtual FReply NativeOnMouseWheel(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override;

可以将接口收束在一个 InputManager 中,向外暴露相关事件;

DragDrop

DragArea 用于维护可拖动的范围,以及封装引擎基本接口、提供注册方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
-- DragArea

--region ----- Events -----

function DragAreaView:OnMouseButtonDown(Geometry, MouseEvent)
return UE4.UWidgetBlueprintLibrary.DetectDragIfPressed(MouseEvent, self, "LeftMouseButton")
end

-- 开始拖动
function DragAreaView:OnDragDetected(Geometry, PointerEvent)
local OperationClass = self.DragDropOperationClass
local Operation = NewObject(OperationClass)

if self.DragDetectedDelegate ~= nil then
for Object, Callback in pairs(self.DragDetectedDelegate) do
xpcall(Callback, error_handler, Object, Geometry, PointerEvent, Operation)
end
end

return Operation
end

-- 取消拖动
function DragAreaView:OnDragCancelled(Geometry, PointerEvent, Operation)
if self.DragCancelledDelegate ~= nil then
for Object, Callback in pairs(self.DragCancelledDelegate) do
xpcall(Event, error_handler, Object, Geometry, PointerEvent, Operation)
end
end
end

-- Other...

--endregion Events

--reigon ----- Bind -----
function DragAreaView:BindEvent(EventName, Object, Callback)
if self[EventName] == nil then self[EventName] = {} end

if self[EventName][Object] == nil then
self[EventName][Object] = Callback
end
end

function DragAreaView:UnbindEvent(EventName, Object)
if self[EventName] == nil then self[EventName] = {} end

if self[EventName][Object] ~= nil then
self[EventName][Object] = nil
end
end

function DragAreaView:BindDragDetected(Object,Func) self:BindEvent("DragDetectedDelegate", Object, Func) end
function DragAreaView:UnbindDragDetected(Object) self:UnbindEvent("DragDetectedDelegate", Object) end

function DragAreaView:BindDragCancelled(Object,Func) self:BindEvent("DragCancelledDelegate", Object, Func) end
function DragAreaView:UnbindDragCancelled(Object) self:UnbindEvent("DragCancelledDelegate", Object) end

--endregion Bind

DragItem 用于维护拖动的实际 Item

PreviewItem 用于维护拖动时的 Preview(也就是拖动时显示的 Item 预览);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
-- DragItem

function DragItemView:Construct()
self.DragArea:BindDragDetected(self, self.OnDragDetectedCallback)
self.DragArea:BindDragCancelled(self, self.OnDragCancelledCallback)
end

function DragItemView:Destruct()
self.DragArea:UnbindDragDetected(self)
self.DragArea:UnbindDragCancelled(self)
end

--
function DragItemView:OnDragDetectedCallback(Geometry,PointerEvent,Operation)

local BPClass = UE.UGameplayStatics.GetObjectClass(self)
Operation.PreviewItem = UE.UCommonWidgetUtils.CreateWidget(BPClass)

Operation.DefaultDragVisual = Operation.PreviewItem
if Operation.PreviewItem and Operation.PreviewItem.OnDragDetected then
Operation.PreviewItem:OnDragDetected()
end

Operation.Pivot = UE.EDragPivot.TopLeft

local LocalPosition = UE.USlateBlueprintLibrary.AbsoluteToLocal(self:GetCachedGeometry(), UE.UKismetInputLibrary.PointerEvent_GetScreenSpacePosition(PointerEvent))

local Offset = UE.FVector2D()
local Size = UE.USlateBlueprintLibrary.GetLocalSize(self:GetCachedGeometry())
Offset.X = LocalPosition.X / Size.X * -1
Offset.Y = LocalPosition.Y / Size.Y * -1
Operation.Offset = Offset

Operation.UseInAnimation = false
end

function DragItemView:OnDragCancelledCallback(Geometry, PointerEvent, Operation)

if Operation.PreviewItem and Operation.PreviewItem.OnDragCancelled then
Operation.PreviewItem:OnDragCancelled()
end

UE.UCommonWidgetUtils.DestroyWidget(Operation.PreviewItem)
end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// CommonWidgetUtils

UUserWidget* UCommonWidgetUtils::CreateWidget(UWorld* InWorld, TSubclassOf<UUserWidget> WidgetType)
{
UUserWidget* Widget = UWidgetBlueprintLibrary::Create(InWorld, WidgetType, nullptr);
AddObjectRef(Widget);
return Widget;
}

void UCommonWidgetUtils::DestroyWidget(UUserWidget* Widget)
{
RemoveObjectRef(Widget);
}

// --------------

void UCommonWidgetUtils::AddObjectRef(UObject* Object)
{
if (Object && IsUObjectValid(Object))
{
const auto L = UnLua::GetState();
auto& Env = UnLua::FLuaEnv::FindEnvChecked(L);
Env.AddManualObjectReference(Object);
}
}

void UCommonWidgetUtils::RemoveObjectRef(UObject* Object)
{
if (Object && IsUObjectValid(Object))
{
const auto L = UnLua::GetState();
auto& Env = UnLua::FLuaEnv::FindEnvChecked(L);
Env.RemoveManualObjectReference(Object);
}
}

特别地:

  1. 通过 OperationDefaultDragVisual 来设置拖动时的预览 Widget,在 FUMGDragDropOp::New 时,将其设置到 Operation->DecoratorWidget,并在 FUMGDragDropOp::OnDragged 时更新 Position
  2. CreateWidget / DestroyWidget 针对 PreviewItem,这里可以额外维护一个 Pool 进行 Widget 复用;
  3. 在合适的地方增加额外 Callback

Touch

引擎对于 Touch 的相关实现主要在:UPlayerInput::InputTouch(uint32 Handle, ETouchType::Type Type, const FVector2D& TouchLocation, float Force, FDateTime DeviceTimestamp, uint32 TouchpadIndex) 中;

实际上,可以根据 IInputProcessor 提供的接口,在 InputManager 中维护 TArray <FInputTouchData> InputTouchDatas 以及相关信息,统一管理并分发给各个业务模块,同时方便自定义与扩展。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// InputTouchData
struct FInputTouchData
{
ETouchIndex::Type TouchIndex;
FVector2D StartLocation;
FVector2D Location;
FVector2D DeltaMove;
float BeginTime;
float UpdateTime;
float DeltaTime;
float ForceValue;
bool bPendingKill;

// ....
}

需要和引擎一样维护所有的 TouchData,这样才可以通过 TouchIndex 与其它的接口关联,对应到其它回调的 PointerEvent 中的 (ETouchIndex::Type)PointerEvent.GetPointerIndex()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// InputProcessor

class FInputProcessor
{
// ...
TArray <FInputTouchData> InputTouchDatas;
}

FInputTouchData FInputProcessor::GetInputTouchData(ETouchIndex::Type TouchIndex)
{
FInputTouchData* DataPtr = InputTouchDatas.FindByKey(TouchIndex);
return DataPtr != nullptr ? *DataPtr : {};
}

// ----- Process -----

void FInputProcessor::ProcessMouseEvent(ETouchType TouchType, const FPointerEvent& MouseEvent)
{
FVector2D OutPixelPosition = FVector2D::ZeroVector;
FVector2D OutViewportPosition = FVector2D::ZeroVector;
USlateBlueprintLibrary::AbsoluteToViewport(GetWorld(), MouseEvent.GetScreenSpacePosition(), OutPixelPosition, OutViewportPosition);

InputTouch(MouseEvent.GetPointerIndex(), TouchType, OutPixelPosition, MouseEvent.GetTouchForce());
}

bool FInputProcessor::HandleMouseButtonDownEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent)
{
ProcessMouseEvent(ETouchType::Began, MouseEvent);
// BrodcastEvents..
return true;
}

bool FInputProcessor::HandleMouseMoveEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent)
{
ProcessMouseEvent(ETouchType::Moved, MouseEvent);
// BrodcastEvents..
return true;
}

bool FInputProcessor::HandleMouseButtonUpEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent)
{
ProcessMouseEvent(ETouchType::Ended, MouseEvent);
// BrodcastEvents..

// Clear PendingKill Datas
for (int Index = InputTouchDatas.Num() - 1; Index >= 0; Index--)
{
if (InputTouchDatas[Index].bPendingKill == true)
{
InputTouchDatas.RemoveAt(Index);
}
}
return true;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// InputProcessor

void FInputProcessor::InputTouch(uint32 Handle, ETouchType::Type Type, const FVector2D& TouchLocation, float Force)
{
FVector Location(TouchLocation, Force);

switch (Type)
{
case ETouchType::Began:
BeginTouch((ETouchIndex::Type)Handle, Location);
break;
case ETouchType::Ended:
EndTouch((ETouchIndex::Type)Handle, Location);
break;
default:
MoveTouch((ETouchIndex::Type)Handle, Location);
break;
}
}

// -----
void FInputProcessor::BeginTouch(const ETouchIndex::Type TouchIndex, const FVector Location)
{
FInputTouchData* DataPtr = InputTouchDatas.FindByKey(TouchIndex);
if (DataPtr == nullptr)
{
InputTouchDatas.AddDefaulted(1);
DataPtr = &InputTouchDatas.Last();
}

FInputTouchData& InputTouchData = *DataPtr;
InputTouchData.TouchIndex = TouchIndex;
InputTouchData.StartLocation = InputTouchData.Location = FVector2D(Location.X, Location.Y);
InputTouchData.ForceValue = Location.Z;
InputTouchData.BeginTime = InputTouchData.UpdateTime = GetWorld()->GetTimeSeconds();
}

void FInputProcessor::MoveTouch(const ETouchIndex::Type TouchIndex, const FVector Location)
{
FInputTouchData* DataPtr = InputTouchDatas.FindByKey(TouchIndex);
if (DataPtr == nullptr) return;

FInputTouchData& InputTouchData = *DataPtr;
FVector2D ScreenLocation = FVector2D(Location.X, Location.Y);
InputTouchData.DeltaMove = ScreenLocation - InputTouchData.Location;
InputTouchData.Location = ScreenLocation;
InputTouchData.ForceValue = Location.Z;
InputTouchData.DeltaTime = GetWorld()->GetDeltaSeconds();
InputTouchData.UpdateTime = GetWorld()->GetTimeSeconds();
}

void FInputProcessor::EndTouch(const ETouchIndex::Type TouchIndex, const FVector Location)
{
for (auto& InputTouchData : InputTouchDatas)
{
if (InputTouchData.TouchIndex == TouchIndex)
{
InputTouchData.bPendingKill = true;
}
}
}

这样就在 InputProcessor 中维护了所有的 TouchDatas,可以分发事件给外部监听,以及提供数据查询。

Gesture

根据 Touch 的信息,通过 FGestureRecognizer 维护 Gesture_PinchGesture_FlickGesture_Rotate 等;

UPlayerInput::Tick 中会进行 GestureRecognizer.DetectGestures(Touches, this, DeltaTime)

  1. 对于 Pinch:如果 float* CurrentAlpha = CurrentGestureValues.Find(EKeys::Gesture_Pinch)Pinch 还未开启,并且本次 TouchCount > 2,则认为 Pinch 刚刚开始,此时根据 CurrentPinchPoint_StartCurrentPinchPoint_End 计算出 AnchorDistanceSq 并存储,将 1.0f 作为基础缩放值存储在 GestureValue;在下一次根据新的 NewDistanceSq / AnchorDistanceSq 作为缩放值记录在 CurrentGestureValues[EKeys::Gesture_Pinch]TouchCount 减少到 < 2 时, Pinch 结束;
  2. 对于 Flick:要求 FlickTime < 0.25f && (FlickCurrent - AnchorPoints[0]).SizeSquared() > 10000.f,经过的时间在 0.25s 内,并且滑动一定的距离;计算和存储 AngleCurrentGestureValues[EKeys::Gesture_Flick]
  3. 对于 Rotate:如果 float* CurrentAngle = CurrentGestureValues.Find(EKeys::Gesture_Rotate)Rotate 还未开启,当 TouchCount > 2 时,则认为 Rotate 刚刚开始,此时计算出 StartAngle 记录,将 0.0f 作为基础旋转值存储在 GestureValue;在后续更新 GestureValueNewAngle - StartAngle,表示相对角度; TouchCount 减少到 < 2 时, Rotate 结束;

最后:

Gesture CurrentGestureValue : Type
Pinch Scale (相对开始的比例缩放)
Flick Angle(本次滑动的角度)
Rotate Angle (相对开始的角度)
1
2
3
4
5
6
7
8
9
bool UPlayerInput::InputGesture(const FKey Gesture, const EInputEvent Event, const float Value)
{
FKeyState& KeyState = KeyStateMap.FindOrAdd(Gesture);

KeyState.Value.X = KeyState.RawValue.X = KeyState.RawValueAccumulator.X = Value;
KeyState.EventAccumulator[Event].Add(++EventCount);

return true;
}

这些信息会存储在 KeyState 里,通过 UPlayerInput::GetKeyState(FKey InKey) 获取;

当然,在 Mouse 的回调中:

1
2
3
virtual FReply NativeOnTouchStarted(const FGeometry& InGeometry, const FPointerEvent& InGestureEvent) override;
virtual FReply NativeOnTouchMoved(const FGeometry& InGeometry, const FPointerEvent& InGestureEvent) override;
virtual FReply NativeOnTouchEnded(const FGeometry& InGeometry, const FPointerEvent& InGestureEvent) override;

可以看到这里有 GestureEvent,也可以获取对应主要信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
FVector2D ScreenSpacePosition;
FVector2D LastScreenSpacePosition;
FVector2D CursorDelta;
const TSet<FKey>* PressedButtons;
FKey EffectingButton;
uint32 PointerIndex;
uint32 TouchpadIndex;
float Force;
bool bIsTouchEvent;
EGestureEvent GestureType;
FVector2D WheelOrGestureDelta;
bool bIsDirectionInvertedFromDevice;
bool bIsTouchForceChanged;
bool bIsTouchFirstMove;

实际上,可以将这里的 GestureRecognizer 移到统一维护的 InputManager 中,将相关信息提供给各个业务查询,方便自定义与扩展:比如增加 Tag 限定提供给外部设置,当且仅当 Tag 一致时,进行对应 Gesture 更新等。

参考

UE 5.4 源码